﻿using System;
using System.Data;
using System.Data.SqlClient;
using System.Web;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Security.Principal;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Text.RegularExpressions;

namespace NotarialService.Classes
{
    public static class ADUser
    {
        // условие для выборки заблокированных пользователей из AD
        private static string blockParam = "userAccountControl:1.2.840.113556.1.4.803:=2";

        // общий метод для добавления или обновления данных пользователя, используется как при логине, так и при работе сервиса импорта AD
        public static SyncResult UpdateUserFromAD(SqlConnection cn, DirectoryEntry domainEntry, DirectoryEntry userEntry, out string errorMessage)
        {
            errorMessage = "";

            if (cn.State != ConnectionState.Open)
            {
                errorMessage = "Соединение с базой данных закрыто!";
                return SyncResult.Error;
            }

            // получение имени пользователя из AD
            string userLogin = GetUserName(userEntry, out errorMessage);
            if (string.IsNullOrEmpty(userLogin))
                return SyncResult.Error;

            // получение имени домена из AD
            string friendlyDomainName = GetDomainName(domainEntry, out errorMessage);
            if (string.IsNullOrEmpty(friendlyDomainName))
                return SyncResult.Error;

            string userFullLogin = string.Format("{0}\\{1}", friendlyDomainName.ToUpper(), userLogin);

            SqlCommand cmd = new SqlCommand("", cn);

            // получение домена
            ADDomainClass adDomain = GetADDomain(friendlyDomainName, cmd, out errorMessage);

            // домен не найден
            if (adDomain.Id == -1)
            {
                errorMessage = "Не найден домен \"" + friendlyDomainName + "\" в базе данных!";
                return SyncResult.Error;
            }

            // ошибка при получении домена
            if (!string.IsNullOrEmpty(errorMessage))
                return SyncResult.Error;
            /*
            // получение групп пользователя
            //List<string> userGroupListAD = GetUserGroupsMemberOf(domainEntry, userEntry, out errorMessage);
            //List<string> userGroupListAD = GetUserGroupsTokenGroups(domainEntry, userEntry, out errorMessage);
            List<string> userGroupListAD = GetUserGroupsFullHierarchy(domainEntry, userEntry, out errorMessage);
            if (!string.IsNullOrEmpty(errorMessage))
                return SyncResult.Error;

            // проверка на вхождение в Allow/Deny группы
            if (!CheckAllowDenyGroups(userLogin, userGroupListAD, adDomain.UserGroupsIn, adDomain.UserGroupsOut, out errorMessage))
                return SyncResult.Error;

            // получение группы
            ADGroupClass adGroup = GetADGroup(adDomain.Id, domainEntry, userEntry, cmd, out errorMessage);
            if (!string.IsNullOrEmpty(errorMessage))
                return SyncResult.Error;

            if (adGroup.Id == -1)
            {
                errorMessage = "Не найдено ни одной группы AD для пользователя, создание/обновление невозможно!";
                return SyncResult.Error;
            }
*/
            SyncResult res = SyncResult.Error;

            // синхронизация с базой
            res = CreateOrUpdateUserFromAD(userLogin, userFullLogin, userEntry, adDomain, cmd, out errorMessage);

            return res;
        }

        public static DirectoryEntry GetUserADEntry(IIdentity identity, out string errorMessage)
        {
            errorMessage = "";

            if (identity.Name.IndexOf('\\') <= 0)
            {
                errorMessage = "В имени пользователя не указан домен!";
                return null;
            }

            SearchResult userSearchResult = null;

            try
            {
                // получение домена
                string friendlyDomainName = identity.Name.Substring(0, identity.Name.IndexOf('\\'));

                DirectoryContext objContext = new DirectoryContext(DirectoryContextType.Domain, friendlyDomainName);
                Domain objDomain = Domain.GetDomain(objContext);

                string name = identity.Name.Substring(identity.Name.LastIndexOf("\\") + 1);

                // поиск пользователя по имени в домене
                DirectorySearcher userSearch = new DirectorySearcher(objDomain.GetDirectoryEntry(), "(&(objectClass=user)(samaccountName=" + name + "))");

                userSearchResult = userSearch.FindOne();
            }
            catch (Exception ex)
            {
                errorMessage = ErrorMessageFromException(ex, "Ошибка при поиске пользователя в Active Directory");
                return null;
            }

            return userSearchResult.GetDirectoryEntry();
        }

        public static DirectoryEntry GetDomainADEntry(IIdentity identity, out string errorMessage)
        {
            DirectoryEntry res = null;
            errorMessage = "";

            if (identity.Name.IndexOf('\\') <= 0)
            {
                errorMessage = "В имени пользователя не указан домен!";
                return null;
            }

            try
            {
                // получение домена
                string friendlyDomainName = identity.Name.Substring(0, identity.Name.IndexOf('\\'));
                res = Domain.GetDomain(new DirectoryContext(DirectoryContextType.Domain, friendlyDomainName)).GetDirectoryEntry();
            }
            catch (Exception ex)
            {
                errorMessage = ErrorMessageFromException(ex, "Ошибка при поиске домена в Active Directory");
                return null;
            }

            return res;
        }

        private static string ParseFieldTemplate(string fieldValue, DirectoryEntry userEntry)
        {
            if (fieldValue == null)
                return null;

            if (fieldValue.IndexOf("{") != -1)
            {
                Regex re = new Regex(@"{(?<params>[\w\.]+)}");
                foreach (Match m in re.Matches(fieldValue))
                {
                    try
                    {
                        fieldValue = fieldValue.Replace("{" + m.Result("${params}") + "}", userEntry.Properties[m.Result("${params}")][0].ToString());
                    }
                    catch
                    {
                        fieldValue = fieldValue.Replace("{" + m.Result("${params}") + "}", "");
                    }
                }
            }

            return fieldValue;
        }

        private static string GetDomainName(DirectoryEntry domainEntry, out string errorMessage)
        {
            errorMessage = "";

            try
            {
                return domainEntry.Properties["name"][0].ToString();
            }
            catch (Exception ex)
            {
                errorMessage = ErrorMessageFromException(ex, "Ошибка получения имени пользователя из Active Directory");
                return "";
            }
        }

        private static string GetUserName(DirectoryEntry userEntry, out string errorMessage)
        {
            errorMessage = "";

            try
            {
                return userEntry.Properties["samaccountname"][0].ToString();
            }
            catch (Exception ex)
            {
                errorMessage = ErrorMessageFromException(ex, "Ошибка получения имени пользователя из Active Directory");
                return "";
            }
        }

        // определение принадлежности пользователя к группе с анализом всей иерархии групп всех типов в AD
        private static bool IsInADRoleFullHierarchy(DirectoryEntry searchRoot, DirectoryEntry userEntry, string role, out string errorMessage)
        {
            errorMessage = "";

            List<string> groups = GetUserGroupsFullHierarchy(searchRoot, userEntry, out errorMessage);

            if (!string.IsNullOrEmpty(errorMessage))
                return false;

            if (groups != null && groups.Contains(role.ToLower()))
                return true;

            return false;
        }
        /*
                // определение принадлежности пользователя к группе по свойству memberOf
                private static bool IsInADRoleMemberOf(DirectoryEntry searchRoot, DirectoryEntry userEntry, string role, out string errorMessage)
                {
                    errorMessage = "";

                    List<string> groups = GetUserGroupsMemberOf(searchRoot, userEntry, out errorMessage);

                    if (!string.IsNullOrEmpty(errorMessage))
                        return false;

                    if (groups.Contains(role.ToLower()))
                        return true;

                    return false;
                }
        */
        // определение принадлежности пользователя к группе по свойству tokenGroups (выбор только Security Groups)
        private static bool IsInADRoleTokenGroups(DirectoryEntry searchRoot, DirectoryEntry userEntry, string role, out string errorMessage)
        {
            errorMessage = "";

            List<string> groups = GetUserGroupsTokenGroups(searchRoot, userEntry, out errorMessage);

            if (!string.IsNullOrEmpty(errorMessage))
                return false;

            if (groups.Contains(role.ToLower()))
                return true;

            return false;
        }

        private static bool IsUserBlocked(DirectoryEntry dirEntry, string username)
        {
            DirectorySearcher dirSearcher = new DirectorySearcher(dirEntry);
            dirSearcher.Filter = "(&(samaccountName=" + username + ")((" + blockParam + ")))";
            SearchResult sr = dirSearcher.FindOne();
            return (sr != null);
        }

        public static string ErrorMessageFromException(Exception ex)
        {
            return ErrorMessageFromException(ex, "Ошибка");
        }

        public static string ErrorMessageFromException(Exception ex, string errorMessage)
        {
            string innerExceptionMessage = "";

            if (ex.InnerException != null)
                innerExceptionMessage = string.Format("\r\n{0}", ex.InnerException.Message);

            return string.Format(
                "{0}:\r\n{1}{2}"
                , errorMessage
                , ex.Message
                , innerExceptionMessage
                );
        }
        /*
                // получение списка элементов по свойству memberOf у пользователя
                public static List<string> GetUserGroupsMemberOf(DirectoryEntry rootDirEntry, string username, out string errorMessage)
                {
                    errorMessage = "";

                    List<string> groups = new List<string>();

                    DirectorySearcher dirSearcher = new DirectorySearcher(rootDirEntry);
                    dirSearcher.Filter = string.Format("(samaccountname={0})", username);

                    DirectoryEntry userEntry = new DirectoryEntry();

                    try
                    {
                        userEntry = dirSearcher.FindOne().GetDirectoryEntry();
                    }
                    catch (Exception ex)
                    {
                        errorMessage = ErrorMessageFromException(ex, "Ошибка при поиска пользователя \"" + username + "\" в Active Directory");
                        return null;
                    }

                    groups = GetUserGroupsMemberOf(rootDirEntry, userEntry, out errorMessage);

                    if (!string.IsNullOrEmpty(errorMessage))
                        return null;

                    return groups;
                }
        */
        // получение списка элементов по свойству memberOf у пользователя
        public static List<string> GetUserGroupsMemberOf(DirectoryEntry rootDirEntry, DirectoryEntry userEntry, out string errorMessage)
        {
            errorMessage = "";

            List<string> groups = new List<string>();
            int propCount;
            try
            {
                //if (!userEntry.Properties.Contains("memberOf"))
                //  return groups;

                propCount = userEntry.Properties["memberOf"].Count;
                string dn;
                int equalsIndex;
                int commaIndex;
                for (int i = 0; i <= propCount - 1; i++)
                {
                    dn = userEntry.Properties["memberOf"][i].ToString();
                    equalsIndex = dn.IndexOf("=", 1);
                    commaIndex = dn.IndexOf(",", 1);

                    if (equalsIndex == -1)
                        return groups;

                    if (!groups.Contains(dn.Substring((equalsIndex + 1), (commaIndex - equalsIndex) - 1)))
                        groups.Add(dn.Substring((equalsIndex + 1), (commaIndex - equalsIndex) - 1).ToLower());
                }
            }
            catch (Exception ex)
            {
                errorMessage = ErrorMessageFromException(ex, "Ошибка при получении memberOf элементов пользователя из Active Directory");
                return groups;
            }

            return groups;
        }

        // получение списка групп пользователя по свойству tokenGroups
        public static List<string> GetUserGroupsTokenGroups(DirectoryEntry rootDirEntry, DirectoryEntry userEntry, out string errorMessage)
        {
            errorMessage = "";

            List<string> res = new List<string>();

            StringBuilder sb = new StringBuilder();

            // формирование строки фильтра для AD
            sb.Append("(|");

            userEntry.RefreshCache(new string[] { "tokenGroups" });

            foreach (byte[] sid in userEntry.Properties["tokenGroups"])
            {
                StringBuilder ossb = new StringBuilder();
                for (int i = 0; i < sid.Length; i++)
                    ossb.AppendFormat("\\{0}", sid[i].ToString("X2"));

                sb.AppendFormat("(objectSid={0})", ossb.ToString());
            }

            sb.Append(")");

            // поиск
            using (DirectorySearcher ds = new DirectorySearcher(rootDirEntry, sb.ToString(), new string[] { "name" }))
            {
                using (SearchResultCollection src = ds.FindAll())
                {
                    for (int i = 0; i < src.Count; i++)
                    {
                        string groupName = src[i].Properties["name"][0].ToString();

                        if (!res.Contains(groupName.ToLower()))
                            res.Add(groupName.ToLower());
                    }
                }
            }

            return res;
        }

        // получение списка групп всех типов у пользователя по свойству memberOf (рекурсивно)
        public static List<string> GetUserGroupsFullHierarchy(DirectoryEntry searchRoot, DirectoryEntry userEntry, out string errorMessage)
        {
            errorMessage = "";

            List<string> res = GetUserGroupsMemberOf(searchRoot, userEntry, out errorMessage);
            string[] arr = res.ToArray();

            for (int i = 0; i < arr.Length; i++)
                foreach (string str in GetGroupMemberOfElementsRecursive(searchRoot, arr[i], out errorMessage))
                {
                    if (!string.IsNullOrEmpty(errorMessage))
                        return new List<string>();

                    if (!res.Contains(str))
                        res.Add(str);
                }

            return res;
        }

        // получение списка групп всех типов у группы по свойству memberOf (рекурсивно)
        public static List<string> GetGroupMemberOfElementsRecursive(DirectoryEntry rootDirEntry, string groupName, out string errorMessage)
        {
            errorMessage = "";

            List<string> groups = new List<string>();
            DirectorySearcher dirSearcher = new DirectorySearcher(rootDirEntry);
            dirSearcher.Filter = string.Format("(&(cn={0})(objectClass=group))", groupName);
            dirSearcher.PropertiesToLoad.Add("memberOf");
            int propCount;
            try
            {
                SearchResult dirSearchResults = dirSearcher.FindOne();

                if (dirSearchResults == null)
                {
                    errorMessage = "Группа \"" + groupName + "\" не найдена в Active Direcory!";
                    return groups;
                }

                propCount = dirSearchResults.Properties["memberOf"].Count;
                string dn;
                int equalsIndex;
                int commaIndex;

                for (int i = 0; i <= propCount - 1; i++)
                {
                    dn = dirSearchResults.Properties["memberOf"][i].ToString();
                    equalsIndex = dn.IndexOf("=", 1);
                    commaIndex = dn.IndexOf(",", 1);

                    if (equalsIndex == -1)
                        return groups;

                    string group = dn.Substring((equalsIndex + 1), (commaIndex - equalsIndex) - 1).ToLower();

                    if (!groups.Contains(group))
                    {
                        groups.Add(group);

                        List<string> children = GetGroupMemberOfElementsRecursive(rootDirEntry, group, out errorMessage);

                        if (children.Count == 0)
                            continue;
                        else
                        {
                            foreach (string child in children)
                                if (!groups.Contains(child))
                                    groups.Add(child);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                errorMessage = ErrorMessageFromException(ex, "Ошибка при получении memberOf элементов группы \"" + groupName + "\" из Active Directory");
                return groups;
            }

            return groups;
        }

        public static bool CheckAllowDenyGroups(string userLogin, List<string> groups, string[] groupsIn, string[] groupsOut, out string errorMessage)
        {
            errorMessage = "";
            bool allowGroupsExist = false;

            // проверка вхождения пользователя хотя бы в одну Deny группу
            if (groupsOut != null)
            {
                foreach (string s in groupsOut)
                {
                    if (!String.IsNullOrEmpty(s) && groups.Contains(s.Trim()))
                    {
                        errorMessage = "Пользователь состоит в запрещенных группах домена!";
                        return false;
                    }
                }
            }

            // проверка вхождения пользователя хотя бы в одну Allow группу
            if (groupsIn != null)
            {
                foreach (string s in groupsIn)
                {
                    if (!String.IsNullOrEmpty(s))
                    {
                        // задана хотя бы одна Allow группа
                        allowGroupsExist = true;

                        if (groups.Contains(s.Trim()))
                            return true;
                    }
                }
            }

            // задана хотя бы одна Allow группа, пользователь в нее не входит
            if (allowGroupsExist)
            {
                errorMessage =
                    string.Format(
                        "{0}\r\n{1}{2}\r\n{3}{4}\r\n{5}{6}"
                        , "Пользователь \"" + userLogin + "\" не состоит в разрешенных группах домена!"
                        , "Группы пользователя из AD: "
                        , string.Join(",", groups.ToArray())
                        , "Разрешенные группы домена: "
                        , string.Join(",", groupsIn)
                        , "Запрещенные группы домена: "
                        , string.Join(",", groupsOut)
                        );

                return false;
            }

            // не задано ни одной Allow группы, на Deny проверка пройдена
            return true;
        }

        public static NameValueCollection GetUserProperties(string userName, string domain)
        {
            NameValueCollection userProperties = new NameValueCollection();
            string filter = string.Format("(&(objectClass={0})(sAMAccountName={1}))", "user", userName);
            try
            {
                using (DirectoryEntry adRoot = new DirectoryEntry("LDAP://" + domain, null, null, AuthenticationTypes.Secure))
                using (DirectorySearcher searcher = new DirectorySearcher(adRoot))
                {
                    searcher.SearchScope = SearchScope.Subtree;
                    searcher.ReferralChasing = ReferralChasingOption.All;
                    searcher.Filter = filter;
                    SearchResult result = searcher.FindOne();
                    if (result != null)
                        using (DirectoryEntry directoryEntry = result.GetDirectoryEntry())
                            foreach (string propName in directoryEntry.Properties.PropertyNames)
                                userProperties.Add(propName, directoryEntry.Properties[propName][0].ToString());
                }
                return userProperties;
            }
            catch
            {
                return null;
            }
        }

        public static DirectoryEntry GetRootDirectoryEntry()
        {
            using (DirectoryEntry defaultServer = new DirectoryEntry("LDAP://rootDSE"))
            {
                string strLdapServer = "";

                try
                {
                    strLdapServer = (string)defaultServer.Properties["defaultNamingContext"].Value;
                }
                catch (Exception ex)
                {
                    return null;
                }

                return new DirectoryEntry("LDAP://" + strLdapServer);
            }
        }

        public static DirectoryEntry GetDomainDirectoryEntry(string domainName)
        {
            DirectoryContext objContext = new DirectoryContext(DirectoryContextType.Domain, domainName);
            Domain objDomain = null;
            try
            {
                objDomain = Domain.GetDomain(objContext);
            }
            catch
            {
                return null;
            }
            return objDomain.GetDirectoryEntry();
        }

        private static ADDomainClass GetADDomain(string friendlyDomainName, SqlCommand cmd, out string errorMessage)
        {
            errorMessage = "";
            ADDomainClass res = new ADDomainClass();
            res.Id = -1;

            // получение домена
            cmd.CommandText = "select Id, [UserName], Email, Phone from dbo.ADDomain where lower([Name]) = @DomainName";
            cmd.Parameters.Clear();
            cmd.Parameters.AddWithValue("@DomainName", friendlyDomainName.ToLower());
            SqlDataReader dataReader = cmd.ExecuteReader();

            if (dataReader.Read())
            {
                res.Id = int.Parse(dataReader["Id"].ToString());
                res.UserName = dataReader["UserName"].ToString();
                res.Email = dataReader["Email"].ToString();
                res.Phone = dataReader["Phone"].ToString();
            }
            else
                errorMessage = "Не найден домен пользователя " + friendlyDomainName + " в базе данных!";

            dataReader.Close();

            return res;
        }

        private static ADGroupClass GetADGroup(int domainId, DirectoryEntry domainEntry, DirectoryEntry userEntry, SqlCommand cmd, out string errorMessage)
        {
            errorMessage = "";
            ADGroupClass res = new ADGroupClass();
            res.DepartmentId = -1;
            res.Id = -1;

            // получение групп
            cmd.CommandText = "select Id, [Name], RoleId, DepartmentId, Comments from dbo.ADGroup where ADDomainId = @DomainId order by SortOrder";
            cmd.Parameters.Clear();
            cmd.Parameters.AddWithValue("@DomainId", domainId);

            DataSet ds = new DataSet();
            SqlDataAdapter da = new SqlDataAdapter(cmd);
            da.Fill(ds);
            DataTable adGroups = ds.Tables[0];

            // прохождение по группам, выбор первой по порядку
            foreach (DataRow group in adGroups.Rows)
            {
                //bool isInGroup = IsInADRoleMemberOf(domainEntry, userEntry, group["Name"].ToString().ToLower(), out errorMessage);
                //bool isInGroup = IsInADRoleTokenGroups(domainEntry, userEntry, group["Name"].ToString().ToLower(), out errorMessage);
                bool isInGroup = IsInADRoleFullHierarchy(domainEntry, userEntry, group["Name"].ToString().ToLower(), out errorMessage);

                if (isInGroup)
                {
                    res.Id = int.Parse(group["Id"].ToString());
                    res.RoleId = int.Parse(group["RoleId"].ToString());

                    cmd.CommandText = "select CompanyId from dbo.ADDomain where Id = @DomainId";
                    res.CompanyId = int.Parse(cmd.ExecuteScalar().ToString());

                    if (group["DepartmentId"] != null && !string.IsNullOrEmpty(group["DepartmentId"].ToString()))
                        res.DepartmentId = int.Parse(group["DepartmentId"].ToString());

                    res.Comments = group["Comments"].ToString();

                    break;
                }
            }

            return res;
        }

        private static SyncResult CreateOrUpdateUserFromAD(string userLogin, string userFullLogin, DirectoryEntry userEntry, ADDomainClass adDomain, SqlCommand cmd, out string errorMessage)
        {
            errorMessage = "";

            // вызов процедуры синхронизации
            cmd.CommandText = "up_CreateOrUpdateUserFromAD";
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.Clear();

            try
            {
                cmd.Parameters.AddWithValue("@Login", userFullLogin);
                cmd.Parameters.AddWithValue("@Password", "password");
                cmd.Parameters.AddWithValue("@UserName", userLogin);
                cmd.Parameters.AddWithValue("@Email", ParseFieldTemplate(adDomain.Email, userEntry) ?? "");
                cmd.Parameters.AddWithValue("@Phone", ParseFieldTemplate(adDomain.Phone, userEntry) ?? "");
                cmd.Parameters.AddWithValue("@Comments", ParseFieldTemplate(adDomain.Comments, userEntry) ?? "");
                cmd.Parameters.AddWithValue("@IsArchive", IsUserBlocked(userEntry, userLogin));
            }
            catch (Exception ex)
            {
                errorMessage = ErrorMessageFromException(ex, "Ошибка получения параметра из Active Directory");
                return SyncResult.Error;
            }

            try
            {
                object procRes = cmd.ExecuteScalar();

                if (procRes == null)
                {
                    errorMessage = "Ошибка обновления пользователя из Active Directory в базе данных!";
                    return SyncResult.Error;
                }
                else
                {
                    switch (procRes.ToString())
                    {
                        case "Created":
                            return SyncResult.Created;
                        case "Updated":
                            return SyncResult.Updated;
                    }
                }
            }
            catch (Exception ex)
            {
                errorMessage = ErrorMessageFromException(ex, "Ошибка обновления пользователя из Active Directory в базе данных");
            }

            return SyncResult.Error;
        }
    }
}

struct ADDomainClass
{
    public int Id, CompanyId;
    public string UserName, Email, Phone, Comments;
    public string[] UserGroupsIn, UserGroupsOut;
}

struct ADGroupClass
{
    public int Id, RoleId, CompanyId, DepartmentId;
    public string Comments;
}

enum AllowDenyGroupsCheckResult
{
    Denied,
    Allowed,
    NotAllowed
}

public enum SyncResult
{
    Created,
    Updated,
    Error
}
